/*
 *  Copyright 2004 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.action.core.contentFieldValue;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.common.parsers.html.HTMLPlainTextExtractor;
import com.blandware.atleap.common.util.ConvertUtil;
import com.blandware.atleap.model.core.*;
import com.blandware.atleap.search.SearchManager;
import com.blandware.atleap.service.core.ContentFieldManager;
import com.blandware.atleap.service.core.LayoutManager;
import com.blandware.atleap.service.core.LookupManager;
import com.blandware.atleap.service.core.PageManager;
import com.blandware.atleap.service.exception.BeanAlreadyExistsException;
import com.blandware.atleap.service.exception.BeanNotFoundException;
import com.blandware.atleap.webapp.action.core.BaseAction;
import com.blandware.atleap.webapp.form.core.ContentFieldValueForm;
import com.blandware.atleap.webapp.struts.HeritableComponentDefinition;
import com.blandware.atleap.webapp.util.core.*;
import org.apache.commons.validator.GenericValidator;
import org.apache.struts.action.*;
import org.apache.struts.tiles.TilesUtil;
import org.springframework.orm.ObjectOptimisticLockingFailureException;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.ByteArrayInputStream;
import java.util.*;

/**
 * <p>Creates or updates content field value
 * </p>
 * <p><a href="CreateOrUpdateContentFieldValueAction.java.html"><i>View Source</i></a></p>
 * <p/>
 *
 * @author Sergey Zubtsovskiy <a href="mailto:sergey.zubtsovskiy@blandware.com">&lt;sergey.zubtsovskiy@blandware.com&gt;</a>
 * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.23 $ $Date: 2008/02/07 16:55:29 $
 * @struts.action path="/core/contentFieldValue/createOrUpdate"
 * name="contentFieldValueForm"
 * scope="request"
 * input="inputForward"
 * validate="true"
 * roles="core-contentFieldValue-createOrUpdate"
 * @struts.action-forward name="inputForward"
 * path=".core.contentFieldValue.createOrUpdate"
 * @struts.action-forward name="listContentFieldValues"
 * path="/core/contentFieldValue/list.do"
 * redirect="false"
 * @struts.action-forward name="callCreateOrUpdateContentFieldValue"
 * path="/core/contentFieldValue/callCreateOrUpdate.do"
 * redirect="false"
 * @struts.action-forward name="listContentFields"
 * path="/core/contentField/list.do"
 * redirect="false"
 */
public final class CreateOrUpdateContentFieldValueAction extends BaseAction {
	/**
	 * @param mapping  The ActionMapping used to select this instance
	 * @param form     The optional ActionForm bean for this request (if any)
	 * @param request  The HTTP request we are proceeding
	 * @param response The HTTP response we are creating
	 * @return an ActionForward instance describing where and how
	 *         control should be forwarded, or null if response
	 *         has already been completed
	 */
	public ActionForward execute(ActionMapping mapping, ActionForm form,
	                             HttpServletRequest request, HttpServletResponse response) throws Exception {

        HttpSession session = request.getSession();
        ServletContext servletContext = session.getServletContext();
        ContentFieldValueForm contentFieldValueForm = (ContentFieldValueForm) form;

        boolean creatingField = !GenericValidator.isBlankOrNull(contentFieldValueForm.getOwnerId())
                && !GenericValidator.isBlankOrNull(contentFieldValueForm.getFieldType());
        boolean creatingIndexedFV = creatingField
                && !GenericValidator.isBlankOrNull(contentFieldValueForm.getIndexedIdentifier());
        
        Long contentFieldId = null;
        Long ownerId = null;
        String identifier = null;
        String indexedIdentifier = null;
        byte fieldType = ContentField.LINE_TYPE;
        if (creatingField) {
            ownerId = Long.valueOf(contentFieldValueForm.getOwnerId());
            fieldType = Byte.valueOf(contentFieldValueForm.getFieldType())
                    .byteValue();
            if (fieldType != ContentField.LINE_TYPE
                    && fieldType != ContentField.MULTILINE_TYPE
                    && fieldType != ContentField.HTML_TYPE) {
                fieldType = ContentField.LINE_TYPE;
            }
            if (creatingIndexedFV) {
                indexedIdentifier = contentFieldValueForm.getIndexedIdentifier();
            } else {
                identifier = contentFieldValueForm.getIdentifier();
            }
        } else {
            if ( !GenericValidator.isBlankOrNull(contentFieldValueForm.getContentFieldId()) ) {
                contentFieldId = Long.valueOf(contentFieldValueForm.getContentFieldId());
            } else if ( session.getAttribute(WebappConstants.CONTENT_FIELD_ID_KEY) != null ) {
                contentFieldId = (Long) session.getAttribute(WebappConstants.CONTENT_FIELD_ID_KEY);
            } else {
                if ( log.isWarnEnabled() ) {
                    log.warn("Missing content field ID. Returning to index...");
                }
                return mapping.findForward("admin");
            }
        }

        if (creatingField &&
                !request.isUserInRole("core-contentField-create")) {
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
			return null;
        }

        if (creatingIndexedFV &&
                !request.isUserInRole("core-contentField-createIndexed")) {
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
			return null;
        }

        if (!creatingIndexedFV && !creatingField) {
            session.setAttribute(WebappConstants.CONTENT_FIELD_ID_KEY,
                                 contentFieldId);
        }

        if ( !isCancelled(request) && session.getAttribute(WebappConstants.CONTENT_FIELD_VALUE_SUBMITTED_ACTION_KEY) == null ) {

			if ( contentFieldValueForm.getIdMap() == null || contentFieldValueForm.getIdMap().isEmpty() ) {
				if ( log.isWarnEnabled() ) {
					log.warn("Missing content field value ID. Returning to list");
				}
				return mapping.findForward("listContentFieldValues");
			}

			session.setAttribute(WebappConstants.CONTENT_FIELD_VALUE_SUBMITTED_ACTION_KEY, "update");

			ContentFieldManager contentFieldManager = (ContentFieldManager) getBean(Constants.CONTENT_FIELD_MANAGER_BEAN);
			List existentCFVLocales = new ArrayList();
			Map alreadyUpdatedValues = new HashMap();

            ContentField contentField;
            if (creatingField) {
                // checking for owner existence
                LookupManager lookupManager = (LookupManager) getBean(Constants.LOOKUP_MANAGER_BEAN);
                Localizable owner = lookupManager.retrieveLocalizable(ownerId);
                if (owner == null) {
                    // owner not found. it might has already been deleted
                    ActionMessages errors = new ActionMessages();
                    errors.add("ownerNotFound", new ActionMessage("core.contentField.errors.ownerNotFound"));
                    saveErrors(request, errors);
                    return mapping.findForward("admin");
                }

                double index = 0;
                if (creatingIndexedFV) {
                    // computing free index
                    index = 0;
                    double pressedIndex = Double.parseDouble(contentFieldValueForm.getCurrentIndex());
                    LayoutManager layoutManager = (LayoutManager) getBean(Constants.LAYOUT_MANAGER_BEAN);
                    Collection allFields = null;

                    List layouts = new ArrayList();
                    String definition = contentFieldValueForm.getDefinition();
                    String tmpDefinition = definition;
                    do {
                        Layout layout = layoutManager.findLayoutByDefinition(tmpDefinition);
                        if ( layout != null ) {
                            layouts.add(layout);
                        }
                        tmpDefinition = ((HeritableComponentDefinition) TilesUtil.getDefinition(tmpDefinition, request, servletContext)).getExtends();
                    } while ( tmpDefinition != null );

                    Collections.reverse(layouts);
                    for ( int i = 0; i < layouts.size(); i++ ) {
                        Layout layout = (Layout) layouts.get(i);
                        List fields = contentFieldManager.findIndexedContentFieldsByLayoutDefinition(indexedIdentifier, layout.getDefinition());
                        allFields = WebappUtil.joinFields(allFields, fields);
                    }

                    if (owner instanceof Page) {
                        Page page = (Page) owner;
                        String pageUri = page.getUri();
                        List pageFields = contentFieldManager.findIndexedContentFieldsByPageUri(indexedIdentifier, pageUri);
                        allFields = WebappUtil.joinFields(allFields, pageFields);
                    }

                    if ( allFields != null ) {
                        List sortedFields = new LinkedList(allFields);
                        Collections.sort(sortedFields, new WebappUtil.IndexedContentFieldComparator());

                        for ( int i = 0; i < sortedFields.size(); i++ ) {
                            ContentField field = (ContentField) sortedFields.get(i);
                            String indexValue = WebappUtil.getContentFieldIndex(field.getIdentifier());
                            double currentIndex = Double.parseDouble(indexValue);
                            if (currentIndex >= pressedIndex) {
                                // stopping: this is what we are looking for
                                if (i < sortedFields.size() - 1) {
                                    // there's next index
                                    ContentField nextField = (ContentField) sortedFields.get(i + 1);
                                    String nextIndexValue = WebappUtil.getContentFieldIndex(nextField.getIdentifier());
                                    double nextIndex = Double.parseDouble(nextIndexValue);
                                    index = (currentIndex + nextIndex) / 2;
                                } else {
                                    // it's last index
                                    index = currentIndex + 1;
                                }
                                break;
                            }
                        }
                    }
                }

                // creating a content field
                contentField = new ContentField();
                if (creatingIndexedFV) {
                    contentField.setIdentifier(indexedIdentifier + "[" + index + "]");
                } else {
                    contentField.setIdentifier(identifier);
                }
                contentField.setInternal(Boolean.FALSE);
                contentField.setType(fieldType);
                try {
                    contentFieldId = contentFieldManager.createContentField(contentField, ownerId);
                    contentField = contentFieldManager.retrieveContentField(contentFieldId);
                } catch (BeanNotFoundException e) {
                    // owner not found. it might has already been deleted
                    ActionMessages errors = new ActionMessages();
                    errors.add("ownerNotFound", new ActionMessage("core.contentField.errors.ownerNotFound"));
                    saveErrors(request, errors);
                    return mapping.findForward("admin");
                } catch (BeanAlreadyExistsException e) {
                    // contentField already exists
                    ActionMessages errors = new ActionMessages();
                    errors.add("contentFieldAlreadyExists", new ActionMessage("core.contentField.errors.alreadyExists"));
                    saveErrors(request, errors);
                    return mapping.findForward("admin");
                }

                session.setAttribute(WebappConstants.CONTENT_FIELD_ID_KEY,
                                     contentFieldId);
            } else {
                contentField = contentFieldManager.retrieveContentField(contentFieldId);
            }

            if ( contentField == null ) {
				// content field not found. it might has already been deleted
				ActionMessages errors = new ActionMessages();
				errors.add("contentFieldNotFound", new ActionMessage("core.contentField.errors.notFound"));
				saveErrors(request, errors);
				return mapping.findForward("listContentFields");
			}

            SearchManager searchManager = SearchManager.getInstance(session.getServletContext());
            
            Localizable owner = contentField.getOwner();
            boolean cfvInDefaultLocaleModifiedForPage = false;
            boolean cfvModifiedForLayout = false;
            boolean cfvCreatedForLayout = false;

            // flushing indices
            CacheUtil cacheUtil = CacheUtil.getInstance(request);
            cacheUtil.flushFieldIndices();

            for ( Iterator i = contentFieldValueForm.getIdMap().keySet().iterator(); i.hasNext(); ) {
				String localeIdentifier = (String) i.next();
				Long cfvId = Long.valueOf((String) contentFieldValueForm.getId(localeIdentifier));

				// flag to determine what action to perform on CFV: create or update
				boolean existentCFV = cfvId.longValue() != -1;
				ContentFieldValue contentFieldValue = null;

				if ( existentCFV ) {
					contentFieldValue = contentFieldManager.retrieveContentFieldValue(cfvId);
				}

				if ( contentFieldValue == null && !GenericValidator.isBlankOrNull((String) contentFieldValueForm.getValue(localeIdentifier)) ) {
					// CFV does not exist: it might has already been deleted or has not been created yet
					// but value is specified so we must create new
					contentFieldValue = new ContentFieldValue();
					existentCFV = false;
				}

				if ( contentFieldValue != null ) {
					String value = (String) contentFieldValueForm.getValue(localeIdentifier);
					String newValue;

					if ( contentField.getType() == ContentField.LINE_TYPE ) {
						newValue = contentFieldValue.getSimpleValue();
						contentFieldValue.setSimpleValue(value);
					} else {
						newValue = ConvertUtil.convertToString(contentFieldValue.getValue());
						contentFieldValue.setValue(ConvertUtil.convertToByteArray(value));
						if ( contentField.getType() == ContentField.HTML_TYPE ) {
							// get all refs
							HTMLPlainTextExtractor extractor = new HTMLPlainTextExtractor();
							Set refs = new HashSet(extractor.extractAllRefs(new ByteArrayInputStream(ConvertUtil.convertToByteArray(value)), Constants.DEFAULT_ENCODING));

							List linkedPages = new ArrayList();
							List linkedResources = new ArrayList();
							for ( Iterator j = refs.iterator(); j.hasNext(); ) {
								String ref = (String) j.next();
								BaseObject refObj = WebappUtil.lookupObject(ref, session.getServletContext(), request.getContextPath(), request);
								if ( refObj != null ) {
									if ( refObj instanceof ContentResource ) {
										linkedResources.add(refObj);
									} else if ( refObj instanceof Page ) {
										linkedPages.add(refObj);
									}
								}
							}

							contentFieldValue.setLinkedResources(linkedResources);
							contentFieldValue.setLinkedPages(linkedPages);
						}
					}

					contentFieldValue.setVersion(Long.valueOf((String) contentFieldValueForm.getVersion(localeIdentifier)));

					try {

						if ( existentCFV ) {
							contentFieldManager.updateContentFieldValue(contentFieldValue, contentFieldId, localeIdentifier);
						} else {
							contentFieldManager.createContentFieldValue(contentFieldValue, contentFieldId, localeIdentifier);
						}

						// put value in cache and into search index

						if ( owner instanceof Layout ) {
                            cfvModifiedForLayout = true;
                            if (!existentCFV) {
                                cfvCreatedForLayout = true;
                            }
                        } else if ( owner instanceof Page ) {
							Page page = (Page) owner;
							CacheUtil.CFVData cfvData = new CacheUtil.CFVData(value, contentFieldId, contentField.getType(), contentFieldValue.getId(), owner.getId());
							cacheUtil.putPageFieldValueInCache(cfvData, page.getUri(), contentField.getIdentifier(), localeIdentifier);
							searchManager.reIndexPage(page, request);
							if ( page instanceof ContentPage ) {
								cacheUtil.updateContentPageLastModifiedInCache(page.getUri());
							}
                            if (contentFieldValue.getContentLocale().getDefaultInstance().booleanValue()) {
                                cfvInDefaultLocaleModifiedForPage = true;
                            }
                        }

					} catch ( BeanAlreadyExistsException e ) {
						existentCFVLocales.add(localeIdentifier);
					} catch ( ObjectOptimisticLockingFailureException e ) {
						String mergedValue;
						if ( contentField.getType() == ContentField.HTML_TYPE ) {
							mergedValue = MergeUtil.mergeHtml(value, newValue);
						} else if ( contentField.getType() == ContentField.MULTILINE_TYPE ) {
							mergedValue = MergeUtil.mergeText(value, newValue);
						} else {
							mergedValue = newValue;
						}

						alreadyUpdatedValues.put(localeIdentifier, mergedValue);
					}
				}
			}

            // CFV in default locale has been changed, so flush cache for all
            // locales for which there is no CFV (because for them CFV in
            // default language is used)
            if (cfvInDefaultLocaleModifiedForPage) {
                Page page = (Page) owner;
                LocaleUtil localeUtil = LocaleUtil.getInstance(servletContext);
                List locales = localeUtil.getAvailableLocales();
                Iterator localeIterator = locales.iterator();
                while (localeIterator.hasNext()) {
                    ContentLocale contentLocale = (ContentLocale) localeIterator.next();
                    String localeIdentifier = contentLocale.getIdentifier();
                    ContentFieldValue anotherValue = (ContentFieldValue) contentField.getContentFieldValuesMap().get(localeIdentifier);
                    if (anotherValue == null) {
                        // no value for this language, so flush its cache
                        // because CFV in default locale has been changed
                        cacheUtil.flushPageFieldValueCache(page.getUri(), contentField.getIdentifier(), localeIdentifier);
                    }
                }
            }

            if (cfvModifiedForLayout) {
                // First, flush cache for this layout.
                // After this, if CFV has been created, flush cache for layout
                // ancestors too because their values could be blocked.
                Layout layout = (Layout) owner;
                String definition = layout.getDefinition();
                do {
                    cacheUtil.flushLayoutFieldValueCache(definition);
                    definition = ((HeritableComponentDefinition) TilesUtil.getDefinition(definition, request, servletContext)).getExtends();
                } while (definition != null && cfvCreatedForLayout);

                //TODO: add into search index for every content page of the layout.
                //TODO: add into index action pages
                List contentPages = layout.getContentPages();
                for ( int j = 0; j < contentPages.size(); j++ ) {
                    ContentPage contentPage = (ContentPage) contentPages.get(j);
                    searchManager.reIndexPage(contentPage, request);
                    cacheUtil.updateContentPageLastModifiedInCache(contentPage.getUri());
                }
            }

            session.removeAttribute(WebappConstants.CONTENT_FIELD_VALUE_SUBMITTED_ACTION_KEY);

            if (owner instanceof Page) {
                Page page = (Page) owner;
                PageManager pageManager = (PageManager) getBean(Constants.PAGE_MANAGER_BEAN);
                page.setLastUpdatedDatetime(new Date());
                pageManager.updatePage(page);
            }

            if ( !existentCFVLocales.isEmpty() || !alreadyUpdatedValues.isEmpty() ) {
				ActionMessages errors = new ActionMessages();
				if ( !existentCFVLocales.isEmpty() ) {
					errors.add("existentCFV", new ActionMessage("core.contentFieldValue.errors.alreadyExists", ConvertUtil.convertListToString(existentCFVLocales, ", ")));
				}
				if ( !alreadyUpdatedValues.isEmpty() ) {
					errors.add("alreadyUpdatedCFV", new ActionMessage("core.contentFieldValue.errors.updateFailed", ConvertUtil.convertListToString(new ArrayList(alreadyUpdatedValues.keySet()), ", ")));
				}
				saveErrors(request, errors);
				request.setAttribute(WebappConstants.REDIRECT_URL_KEY, session.getAttribute(WebappConstants.REDIRECT_URL_KEY));
				if ( !alreadyUpdatedValues.isEmpty() ) {
					List selectedLocales = new ArrayList(alreadyUpdatedValues.keySet());
					request.setAttribute(WebappConstants.CONTENT_FIELD_VALUE_SELECTED_LOCALES_KEY, selectedLocales);
					request.setAttribute(WebappConstants.CONTENT_FIELD_VALUES_MERGED_KEY, alreadyUpdatedValues);
					return mapping.findForward("callCreateOrUpdateContentFieldValue");
				} else {
					return mapping.findForward("listContentFieldValues");
				}
			}
        }

		String requestUrl = (String) session.getAttribute(WebappConstants.REDIRECT_URL_KEY);
		if ( !GenericValidator.isBlankOrNull(requestUrl) ) {
			session.removeAttribute(WebappConstants.REDIRECT_URL_KEY);
			return new ActionForward(requestUrl, true);
		}

		return mapping.findForward("listContentFieldValues");

	}
}
